//
// Tokaplot
// https://code.google.com/p/tokaplot/
//
// License: GPL v3
// https://code.google.com/p/tokaplot/wiki/License
//
// This file is part of Tokaplot.
//
// Tokaplot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Tokaplot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Tokaplot. If not, see <http://www.gnu.org/licenses/>
//

package com.tokaplot;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.widget.RemoteViews;
import android.widget.Toast;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.os.Process;

// import android.util.Log;

public class TokaplotService extends Service {

    // private static final String TAG = "Tokaplot";

    public final static String ACTION_APPWIDGET_CONFIG  = "com.tokaplotplus.TokaplotService.APPWIDGET_CONFIG";
    public final static String ACTION_APPWIDGET_RESET   = "com.tokaplotplus.TokaplotService.APPWIDGET_RESET";
    public final static String ACTION_APPWIDGET_LOCK    = "com.tokaplotplus.TokaplotService.APPWIDGET_LOCK";
    public final static String ACTION_APPWIDGET_UNLOCK  = "com.tokaplotplus.TokaplotService.APPWIDGET_UNLOCK";
    public final static String ACTION_APPWIDGET_PAUSE   = "com.tokaplotplus.TokaplotService.APPWIDGET_PAUSE";
    public final static String ACTION_APPWIDGET_RESUME  = "com.tokaplotplus.TokaplotService.APPWIDGET_RESUME";
    public final static String ACTION_APPWIDGET_NULL    = "com.tokaplotplus.TokaplotService.APPWIDGET_NULL";

    public final static String EXTRA_APPWIDGET_FLAGS    = "com.tokaplotplus.TokaplotService.APPWIDGET_FLAGS";
    public final static String EXTRA_APPWIDGET_SIZE     = "com.tokaplotplus.TokaplotService.APPWIDGET_SIZE";

    public final static int ACTION_WIDGET_START     = 0;
    public final static int ACTION_WIDGET_STOP      = 1;
    public final static int ACTION_WIDGET_CONFIG    = 2;
    public final static int ACTION_WIDGET_RESET     = 3;
    public final static int ACTION_WIDGET_LOCK      = 4;
    public final static int ACTION_WIDGET_UNLOCK    = 5;
    public final static int ACTION_WIDGET_PAUSE     = 6;
    public final static int ACTION_WIDGET_RESUME    = 7;

    // Widget thread tables
    private Handler handler = new Handler();
    private SyncRunnable runnerSync = null;
    private Hashtable<Integer, WorkerRunnable> runnerTable = new Hashtable<Integer, WorkerRunnable>();
    private Hashtable<Integer, WidgetInfo> widgetInfoTable = new Hashtable<Integer, WidgetInfo>();

    // Managers for various sensors
    private TelephonyManager telManager = null;
    private SensorManager sensorManager = null;
    private LocationManager locManager  = null;

    // Diagnostic info
    public static final DiagnosticInfo dInfo = new DiagnosticInfo();

    // Hard limits
    public final static int maxDataLength   = 4000;     // Maximum number of samples
    public final static float maxVal        = 1.0E5F;   // Maximum allowable sensor value
    private final static float signalMin    = -200.0f;  // Minimum signal in dBm    

    // Global parameters
    private final int   notificationID      = 1;        // Internal ID for notification
    private final long  syncRateDefault     = 60000;    // Default update of log readers (1 minute)
    private long        syncRate            = 60000;    // Initial sampling rate for all widgets
    private boolean     lockWidgets         = false;    // Whether widgets are locked
    private boolean     pauseWidgets        = false;    // Whether updating is paused
    private int         wLastID             = -1;       // Last widget ID edited (only for copying draw style)

    // Log readers
    private ReadCPU     readCPU     = new ReadCPU();
    private ReadCPULoad readCPULoad = new ReadCPULoad();
    private ReadNetwork readNet     = new ReadNetwork();
    private ReadMemory  readMem     = new ReadMemory();
    private ReadCurrent readCurrent = new ReadCurrent();
    
    // Sensor table
    public static LinkedHashMap<String, SensorInfo> sensorTable = new LinkedHashMap<String, SensorInfo>();
    
    // Sensor names; sensors are uniquely identified using this name 
    final public static String s_none               = "None";
    final public static String s_battery_level      = "Battery level (%)";
    final public static String s_temperature        = "Temperature (C)";
    final public static String s_cpu_usage          = "CPU usage (%)";
    final public static String s_process_activity   = "Process activity";
    final public static String s_cpu_load           = "CPU load";
    final public static String s_memory_free        = "Memory free (%)";
    final public static String s_data_in            = "Data in (kB/s)";
    final public static String s_data_out           = "Data out (kB/s)";
    final public static String s_battery_current    = "Battery current (mA)";
    final public static String s_cell_signal        = "Cell signal (dBm)";
    final public static String s_wifi_signal        = "WiFi signal (dBm)";
    final public static String s_gravity            = "Gravity (m/s^2)";
    final public static String s_magnetic_field     = "Magnetic field (uT)";
    final public static String s_light              = "Light (lux)";
    final public static String s_gps_speed          = "GPS speed (m/s)";
    final public static String s_gps_altitude       = "GPS altitude (m)";

    public void initSensors() {
        
        //
        // initSensors() builds the sensor list. The sensor entries are stored in a
        // Map (sensorTable) composed of a string identifying the sensor type and a  
        // SensorInfo item.
        //
        
        // Null sensor
        sensorTable.put(s_none, null);

        // General sensors
        sensorTable.put(s_battery_level, new SensorInfo(maxDataLength, maxVal));
        sensorTable.put(s_temperature, new SensorInfo(maxDataLength, maxVal));

        // Log-type sensors
        if (readCPU.canRead()) {
            sensorTable.put(s_cpu_usage, new SensorInfo(maxDataLength, maxVal));
            sensorTable.put(s_process_activity, new SensorInfo(maxDataLength, maxVal));
        }
        if (readCPULoad.canRead()) {
            sensorTable.put(s_cpu_load, new SensorInfo(maxDataLength, maxVal));
        }
        if (readMem.canRead()) {
            sensorTable.put(s_memory_free, new SensorInfo(maxDataLength, maxVal));
        }
        if (readNet.canRead()) {
            sensorTable.put(s_data_in, new SensorInfo(maxDataLength, maxVal));
            sensorTable.put(s_data_out, new SensorInfo(maxDataLength, maxVal));
        }
        if (readCurrent.canRead()) {
            sensorTable.put(s_battery_current, new SensorInfo(maxDataLength, maxVal));
        }

        // Wireless sensors
        telManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        if (telManager != null) {
            sensorTable.put(s_cell_signal, new SensorInfo(maxDataLength, maxVal));
        }
        sensorTable.put(s_wifi_signal, new SensorInfo(maxDataLength, maxVal));

        // Device sensors
        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        if (sensorManager != null) {
            if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null)
                sensorTable.put(s_gravity, new SensorInfo(maxDataLength, maxVal));
            if (sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null)
                sensorTable.put(s_magnetic_field, new SensorInfo(maxDataLength, maxVal));
            if (sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) != null)
                sensorTable.put(s_light, new SensorInfo(maxDataLength, maxVal));
        }

        // Location sensors
        locManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        if (locManager != null) {
            if (locManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
                sensorTable.put(s_gps_speed, new SensorInfo(maxDataLength, maxVal));
                sensorTable.put(s_gps_altitude, new SensorInfo(maxDataLength, maxVal));
            }
        }

        // Reset all sensors
        resetSensors();
    }

    private boolean usingDeviceSensors = false;

    public static boolean isDeviceSensor(String type) {
        if (type == s_gravity || type == s_magnetic_field || type == s_light)
            return true;
        return false;
    }

    private boolean usingGPSSensors = false;

    public static boolean isGPSSensor(String type) {
        if (type == s_gps_speed || type == s_gps_altitude)
            return true;
        return false;
    }

    public void set(String type, float val) {

        // Set data value for sensor
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            s.set(val);
    }

    public void setAverage(String type, float val) {

        // Set average component of data value
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            s.setAverage(val);
    }

    public static float getSensorMax(String type) {

        // Return maximum data value
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            return s.getMax();
        else
            return maxVal;
    }

    public static float getSensorMin(String type) {

        // Return minimum data value
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            return s.getMin();
        else
            return -maxVal;
    }

    public static float getSensorCurrent(String type) {

        // Return minimum data value
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            return s.getCurrent();
        else
            return 0.0f;
    }

    public static float getBufferFraction(String type) {

        // Return relative current write position within the buffer
        SensorInfo s = sensorTable.get(type);
        if (s != null)
            return s.getBufferFraction();
        else
            return 0.0f;
    }

    public void resetSensors() {

        // Initialise data and current data position
        for (SensorInfo s : sensorTable.values())
            if (s != null)
                s.reset();

        // Set initial values
        updateSyncSensors();

        // Set sensible ranges for certain sensor types
        SensorInfo s;
        s = sensorTable.get(s_battery_level);
        if (s != null)
            s.setMaxMin(100.0f, 0.0f);
        s = sensorTable.get(s_cpu_usage);
        if (s != null)
            s.setMaxMin(100.0f, 0.0f);
        s = sensorTable.get(s_memory_free);
        if (s != null)
            s.setMaxMin(100.0f, 0.0f);
    }

    public static CharSequence[] getSensorStrings() {
        return sensorTable.keySet().toArray(new CharSequence[sensorTable.size()]);
    }

    // Synchronous sensors
    public void updateSyncSensors() {
        
        //
        // updateSyncSensors() updates the synchronous sensor types. These include
        // readings polled from system files.
        //
        
        float[] readElements = new float[2];

        // Read CPU
        if (readCPU.read(readElements)) {
            set(s_cpu_usage, readElements[0]);
            set(s_process_activity, readElements[1]);
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_READ_CPU);
            dInfo.clearError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CPU);
        } else {
            dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_READ_CPU);
            dInfo.setError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CPU);
        }

        // Read CPU load
        if (readCPULoad.read(readElements)) {
            set(s_cpu_load, readElements[0]);
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_READ_CPL);
            dInfo.clearError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CPL);
        } else {
            dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_READ_CPL);
            dInfo.setError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CPL);
        }

        // Read network
        if (readNet.read(readElements)) {
            set(s_data_in, readElements[0] / (float)syncRate);
            set(s_data_out, readElements[1] / (float)syncRate);
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_READ_NET);
            dInfo.clearError(DiagnosticInfo.DIAG_ERROR_FILE_READ_NET);
        } else {
            dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_READ_NET);
            dInfo.setError(DiagnosticInfo.DIAG_ERROR_FILE_READ_NET);
        }

        // Read memory
        if (readMem.read(readElements)) {
            set(s_memory_free, readElements[0]);
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_READ_MEM);
            dInfo.clearError(DiagnosticInfo.DIAG_ERROR_FILE_READ_MEM);
        } else {
            dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_READ_MEM);
            dInfo.setError(DiagnosticInfo.DIAG_ERROR_FILE_READ_MEM);
        }

        // Read battery current
        if (readCurrent.read(readElements)) {
            set(s_battery_current, readElements[0]);
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_READ_CUR);
            dInfo.clearError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CUR);

        } else {
            dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_READ_CUR);
            dInfo.setError(DiagnosticInfo.DIAG_ERROR_FILE_READ_CUR);
        }
    }

    // Service handlers
    @Override
    public void onCreate() {

        Context context = getApplicationContext();
        SharedPreferences mPrefs = getSharedPreferences("TP", Context.MODE_PRIVATE);

        // This is the first time the service has been requested to start
        int priority_Type = mPrefs.getInt("priority_Type", -1);
        if (priority_Type < 0) {
            Process.setThreadPriority(Process.THREAD_PRIORITY_LESS_FAVORABLE);
        } else {
            Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
        }

        // make sure widget is in un-paused state
        SharedPreferences.Editor ed = mPrefs.edit();
        ed.putBoolean("isPaused", false);
        ed.commit();

        // Get pause and lock flags
        boolean isLocked = mPrefs.getBoolean("isLocked", false);
        if (isLocked) {

            // Set lock
            lockWidgets = true;

            // Create unlock intent
            Intent widgetIntent = new Intent();
            widgetIntent.setAction(TokaplotService.ACTION_APPWIDGET_UNLOCK);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, widgetIntent, PendingIntent.FLAG_UPDATE_CURRENT);

            // Set notification
            Notification mNotification = new Notification(R.drawable.sicon1, getString(R.string.app_name) + " widget locked", System.currentTimeMillis());
            mNotification.setLatestEventInfo(context, getText(R.string.app_name), "Click to unlock", pendingIntent);
            mNotification.icon = R.drawable.sicon2;
            mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
            // mNotification.flags |= Notification.FLAG_NO_CLEAR;

            // Show notification
            NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(notificationID, mNotification);
        }

        // Start the main timer
        runnerSync = new SyncRunnable();
        handler.post(runnerSync);

        // Initialise sensors
        initSensors();

        // Register receivers
        IntentFilter iFilter = new IntentFilter();
        iFilter.addAction(Intent.ACTION_SCREEN_ON);
        iFilter.addAction(Intent.ACTION_SCREEN_OFF);
        iFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
        iFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
        iFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        iFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        registerReceiver(onReceiveBroadcast, iFilter);

        // Start cell strength listener
        if (telManager != null) {
            telManager.listen(phoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTH); // for Android 2.1 + only
            dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_TELEPHONY);
        }

        // If there are any saved widget IDs for restart, reload them
        SharedPreferences wPrefs = getSharedPreferences("TP_wIDs", Context.MODE_PRIVATE);
        Map<String, ?> wIDs = wPrefs.getAll();
        if (wIDs.isEmpty() == false) {
            for (String s : wIDs.keySet()) {

                int wID = (Integer) wIDs.get(s);

                WidgetInfo wInfo = null;
                if (s.contains("wID_21"))
                    wInfo = new WidgetInfo(wID, 21);
                else if (s.contains("wID_22"))
                    wInfo = new WidgetInfo(wID, 22);
                else if (s.contains("wID_41"))
                    wInfo = new WidgetInfo(wID, 41);
                else if (s.contains("wID_42"))
                    wInfo = new WidgetInfo(wID, 42);

                // Create new widget
                if (wInfo != null) {
                    processWidgetCommand(ACTION_WIDGET_START, wInfo);
                }
            }

            // Clear saved widget ID list
            ed = wPrefs.edit();
            ed.clear();
            ed.commit();
        }
    }

    public void destroy() {
        
        // Save destroy count
        SharedPreferences mPrefs = getSharedPreferences("TP", Context.MODE_PRIVATE);
        int destroy_Count = mPrefs.getInt("destroy_Count", -1);
        destroy_Count++;
        SharedPreferences.Editor ed = mPrefs.edit();
        ed.putInt("isPaused", destroy_Count);
        ed.commit();
        
        // Unregister receivers
        unregisterReceiver(onReceiveBroadcast);
        if (telManager != null) {
            telManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
        }

        // Unregister GPS
        if (usingGPSSensors) {
            locManager.removeUpdates(locListener);
        }

        // Unregister sensors
        if (usingDeviceSensors) {
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER));
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD));
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT));
        }
    }

    @Override
    public void onStart(Intent intent, int startId) {
        handleStart(intent);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handleStart(intent);
        return START_STICKY;
    }

    public void handleStart(Intent intent) {

        // Check for restart
        if (intent == null) {

            // Refresh all widgets
            refreshAll();

        } else {

            // Read flags from intent
            int appWidgetFlags = intent.getIntExtra(EXTRA_APPWIDGET_FLAGS, -1);
            if (appWidgetFlags == -1) {
                // Unknown flag
                return;
            }

            // Read widget ID from intent
            int wID = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
            if (wID == -1) {

                // No ID for global commands
                processGlobalCommand(appWidgetFlags);

            } else {

                // ID for widget-specific commands
                WidgetInfo wInfo = new WidgetInfo(intent);
                processWidgetCommand(appWidgetFlags, wInfo);
            }
        }

        // Check widget count
        if (runnerTable.isEmpty()) {

            // If there are no timers left, stop the service
            handler.removeCallbacks(runnerSync);
            stopSelf();
        }
    }

    private void processGlobalCommand(int appWidgetFlags) {

        // Global events
        if (appWidgetFlags == ACTION_WIDGET_PAUSE) {

            // Pause widget
            CharSequence text = "Tokaplot widget paused";
            Toast toast = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT);
            toast.show();
            pauseWidgets = true;

            handler.removeCallbacks(runnerSync);
            WorkerRunnable runnerWorker;
            for (Enumeration<WorkerRunnable> e = runnerTable.elements(); e.hasMoreElements();) {
                runnerWorker = e.nextElement();
                runnerWorker.run();
                handler.removeCallbacks(runnerWorker);
            }
            return;

        } else if (appWidgetFlags == ACTION_WIDGET_RESET) {

            // Reset all data
            resetSensors();

        } else if (appWidgetFlags == ACTION_WIDGET_LOCK) {

            // Lock widget
            CharSequence text = "Tokaplot widget locked";
            Toast toast = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT);
            toast.show();
            lockWidgets = true;

            // Create unlock intent
            Context context = getApplicationContext();
            Intent widgetIntent = new Intent();
            widgetIntent.setAction(TokaplotService.ACTION_APPWIDGET_UNLOCK);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, widgetIntent, PendingIntent.FLAG_UPDATE_CURRENT);

            // Set notification
            Notification mNotification = new Notification(R.drawable.sicon1, getString(R.string.app_name) + " widget locked", System.currentTimeMillis());
            mNotification.setLatestEventInfo(context, getText(R.string.app_name), "Click to unlock", pendingIntent);
            mNotification.icon = R.drawable.sicon2;
            mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
            // mNotification.flags |= Notification.FLAG_NO_CLEAR;

            // Show notification
            NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(notificationID, mNotification);

        } else if (appWidgetFlags == ACTION_WIDGET_UNLOCK) {

            // Unlock widget
            CharSequence text = "Tokaplot widget unlocked";
            Toast toast = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT);
            toast.show();
            lockWidgets = false;

            SharedPreferences mPrefs = getSharedPreferences("TP", Context.MODE_PRIVATE);
            SharedPreferences.Editor ed = mPrefs.edit();
            ed.putBoolean("isLocked", false);
            ed.commit();

        } else if (appWidgetFlags == ACTION_WIDGET_RESUME) {

            // Resume widget
            CharSequence text = "Tokaplot widget resumed";
            Toast toast = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT);
            toast.show();
            pauseWidgets = false;

            // Update log runner
            handler.removeCallbacks(runnerSync);
            handler.post(runnerSync);
        }

        // Refresh all widgets
        refreshAll();
    }

    private void processWidgetCommand(int appWidgetFlags, WidgetInfo wInfo) {

        // Local events
        if (runnerTable.containsKey(wInfo.getID())) {

            if (appWidgetFlags == ACTION_WIDGET_STOP) {

                // Remove any preference data
                wInfo.clearPreferences(getApplicationContext());

                // Stop worker thread if requested
                WorkerRunnable runnerWorker = runnerTable.get(wInfo.getID());
                handler.removeCallbacks(runnerWorker);
                runnerTable.remove(wInfo.getID());
                widgetInfoTable.remove(wInfo.getID());

            } else if (appWidgetFlags == ACTION_WIDGET_CONFIG) {

                // Save preference data
                wInfo.savePreferences(getApplicationContext());

                // Remove record in info table and add in record with new info
                widgetInfoTable.remove(wInfo.getID());
                widgetInfoTable.put(wInfo.getID(), wInfo);

                // Update worker runner with new configuration
                WorkerRunnable runnerWorker = runnerTable.get(wInfo.getID());
                handler.removeCallbacks(runnerWorker);
                runnerWorker.configure(wInfo);
                runnerWorker.refresh();
                if (!pauseWidgets)
                    handler.post(runnerWorker);

                // Update log runner
                handler.removeCallbacks(runnerSync);
                if (!pauseWidgets)
                    handler.post(runnerSync);

                // Get log rate
                SharedPreferences mPrefs = getSharedPreferences("TP", Context.MODE_PRIVATE);
                syncRate = mPrefs.getLong("sync_Rate", syncRateDefault);

            } else if (appWidgetFlags == ACTION_WIDGET_START) {

                // Widget re-start due to periodic update
                refreshAll();
            }

        } else {

            if (appWidgetFlags == ACTION_WIDGET_START) {

                // This is a new widget so get any saved data
                wInfo.setIDLast(wLastID);
                wLastID = wInfo.getID();
                wInfo.loadPreferences(getApplicationContext());

                // Start the worker timer
                WorkerRunnable runnerWorker = new WorkerRunnable(new WidgetInfo(wInfo), this, AppWidgetManager.getInstance(this));
                runnerTable.put(wInfo.getID(), runnerWorker);
                widgetInfoTable.put(wInfo.getID(), wInfo);
                if (!pauseWidgets)
                    handler.post(runnerWorker);

                // Unlock all widgets
                SharedPreferences mPrefs = getSharedPreferences("TP", Context.MODE_PRIVATE);
                boolean isLocked = mPrefs.getBoolean("isLocked", false);
                if (isLocked) {
                    Intent widgetIntent = new Intent();
                    widgetIntent.setAction(TokaplotService.ACTION_APPWIDGET_UNLOCK);
                    sendBroadcast(widgetIntent);
                }
            }
        }

        // Check widget count
        if (runnerTable.isEmpty()) {

            // If there are no timers left, stop the service
            handler.removeCallbacks(runnerSync);
            stopSelf();

        } else {

            // Detect whether GPS and sensors are required
            WidgetInfo workerInfo;
            boolean workerNeedsSensors = false;
            boolean workerNeedsGPS = false;
            for (Enumeration<WidgetInfo> e = widgetInfoTable.elements(); e.hasMoreElements();) {
                workerInfo = e.nextElement();
                if (isDeviceSensor(workerInfo.getType1()) || isDeviceSensor(workerInfo.getType2())) {
                    workerNeedsSensors = true;
                }
                if (isGPSSensor(workerInfo.getType1()) || isGPSSensor(workerInfo.getType2())) {
                    workerNeedsGPS = true;
                }
            }
            checkDeviceSensors(workerNeedsSensors);
            checkGPS(workerNeedsGPS);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // Remove any callbacks to log runner
        handler.removeCallbacks(runnerSync);

        // Unregister all receivers
        destroy();

        // Remove any callbacks to widget runners
        if (runnerTable.isEmpty() == false) {
            
            // If the service is ending because the last widget is deleted this should be empty
            // otherwise, the service has been stopped, so save widget IDs for future re-start
            WorkerRunnable runnerWorker;
            SharedPreferences wPrefs = getApplicationContext().getSharedPreferences("TP_wIDs", Context.MODE_PRIVATE);
            SharedPreferences.Editor ed = wPrefs.edit();
            for (Enumeration<WorkerRunnable> e = runnerTable.elements(); e.hasMoreElements();) {
                
                // Remove callback
                runnerWorker = e.nextElement();
                handler.removeCallbacks(runnerWorker);

                // Save widget IDs
                int wID = runnerWorker.getWidgetID();
                int wSize = runnerWorker.getWidgetSize();
                ed.putInt("wID_" + wSize + "_" + wID, wID);
            }
            ed.commit();
        }

        // Clear tables
        runnerTable.clear();
        widgetInfoTable.clear();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    public void onConfigurationChanged(Configuration newConfig) {

        // Refresh all worker runners
        refreshAll();
    }

    public void refreshAll() {

        // Refresh all widgets
        WorkerRunnable runnerWorker;
        for (Enumeration<WorkerRunnable> e = runnerTable.elements(); e.hasMoreElements();) {
            runnerWorker = e.nextElement();
            handler.removeCallbacks(runnerWorker);
            runnerWorker.needsupdate();
            runnerWorker.refresh();
            if (!pauseWidgets)
                handler.post(runnerWorker);
        }
    }

    // Runner handlers
    private class SyncRunnable implements Runnable {

        public void run() {

            // Update log data
            updateSyncSensors();

            // Re-run after fixed time
            handler.removeCallbacks(this);
            handler.postDelayed(this, syncRate);
        }
    }

    private class WorkerRunnable implements Runnable {

        WidgetInfo wInfo;
        Context appContext;
        AppWidgetManager appWidgetManager;
        Render appRender;
        RenderInfo appRenderInfo1;
        RenderInfo appRenderInfo2;
        boolean needsRefresh;

        public WorkerRunnable(WidgetInfo pInfo, Context context, AppWidgetManager appWidgetManager) {

            // Set handles
            wInfo = new WidgetInfo(pInfo);
            this.appWidgetManager = appWidgetManager;
            appContext = context;
            appRender = new Render(pInfo, context);
            needsRefresh = true;

            // Create render resources
            Resources res = context.getResources();
            int maxTime;
            if (wInfo.is22()) {
                maxTime = (int) res.getDimension(R.dimen.layout_22f_port_width);
            } else if (wInfo.is41()) {
                maxTime = (int) res.getDimension(R.dimen.layout_41f_port_width);
            } else if (wInfo.is42()) {
                maxTime = (int) res.getDimension(R.dimen.layout_42f_port_width);
            } else {
                maxTime = (int) res.getDimension(R.dimen.layout_21f_port_width);
            }
            appRenderInfo1 = new RenderInfo(maxTime);
            appRenderInfo2 = new RenderInfo(maxTime);
        }

        public void needsupdate() {

            // Needs configuration update
            wInfo.setNeedsUpdate();
        }

        public void configure(WidgetInfo pInfo) {

            // Update configuration
            wInfo = pInfo;
        }

        public void refresh() {

            // Refresh graph
            needsRefresh = true;
        }

        public int getWidgetID() {

            // Return widget id
            return wInfo.getID();
        }

        public int getWidgetSize() {

            // Return widget size
            if (wInfo.is21())
                return 21;
            else if (wInfo.is22())
                return 22;
            else if (wInfo.is41())
                return 41;
            else if (wInfo.is42())
                return 42;
            return 0;
        }

        public void run() {

            // Update view
            RemoteViews remoteViews = null;
            if (wInfo.is21())
                remoteViews = new RemoteViews(appContext.getPackageName(), R.layout.main21);
            else if (wInfo.is22())
                remoteViews = new RemoteViews(appContext.getPackageName(), R.layout.main22);
            else if (wInfo.is41())
                remoteViews = new RemoteViews(appContext.getPackageName(), R.layout.main41);
            else if (wInfo.is42())
                remoteViews = new RemoteViews(appContext.getPackageName(), R.layout.main42);

            if (remoteViews != null) {
                if (needsRefresh) {
                    appRenderInfo1.makeAll(wInfo.getType1(), wInfo.getSampRate(), wInfo.getUseLocalRange());
                    appRenderInfo2.makeAll(wInfo.getType2(), wInfo.getSampRate(), wInfo.getUseLocalRange());
                } else {
                    appRenderInfo1.make(wInfo.getType1(), wInfo.getSampRate(), wInfo.getUseLocalRange());
                    appRenderInfo2.make(wInfo.getType2(), wInfo.getSampRate(), wInfo.getUseLocalRange());
                }
                appRender.update(remoteViews, wInfo, appRenderInfo1, appRenderInfo2, pauseWidgets);

                // Set diagnostic info
                if (sensorTable.containsKey(wInfo.getType1())) {
                    dInfo.setDataFraction(1, getBufferFraction(wInfo.getType1()));
                    dInfo.setRenderBufferLevel(1, appRenderInfo1.getBufferPercentage());
                } else {
                    dInfo.setDataFraction(1, 0.0f);
                    dInfo.setRenderBufferLevel(1, 0.0f);
                }
                if (sensorTable.containsKey(wInfo.getType2())) {
                    dInfo.setDataFraction(2, getBufferFraction(wInfo.getType2()));
                    dInfo.setRenderBufferLevel(2, appRenderInfo2.getBufferPercentage());
                } else {
                    dInfo.setDataFraction(2, 0.0f);
                    dInfo.setRenderBufferLevel(2, 0.0f);
                }

                // Refresh click handler
                if (!lockWidgets) {
                    Intent widgetIntent = new Intent(appContext, Config.class);
                    wInfo.setIntentWithInfo(widgetIntent);
                    widgetIntent.setData(Uri.parse("widget://widget/id/" + wInfo.getID()));
                    PendingIntent pendingIntent = PendingIntent.getActivity(appContext, 0, widgetIntent, PendingIntent.FLAG_UPDATE_CURRENT);

                    // Widget is unlocked
                    if (wInfo.is21())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_21, pendingIntent);
                    else if (wInfo.is22())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_22, pendingIntent);
                    else if (wInfo.is41())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_41, pendingIntent);
                    else if (wInfo.is42())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_42, pendingIntent);
                } else {

                    // Widget is locked, set click to null broadcast
                    Intent widgetIntent = new Intent();
                    widgetIntent.setAction(ACTION_APPWIDGET_NULL);
                    PendingIntent nullPendingIntent = PendingIntent.getBroadcast(appContext, 0, widgetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
                    if (wInfo.is21())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_21, nullPendingIntent);
                    else if (wInfo.is22())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_22, nullPendingIntent);
                    else if (wInfo.is41())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_41, nullPendingIntent);
                    else if (wInfo.is42())
                        remoteViews.setOnClickPendingIntent(R.id.widget_layout_42, nullPendingIntent);
                }

                // Update widget
                appWidgetManager.updateAppWidget(wInfo.getID(), remoteViews);

                // Re-run after fixed time
                handler.removeCallbacks(this);
                handler.postDelayed(this, wInfo.getSampRate());
                needsRefresh = false;

            } else {

                // Re-try after 1 minute
                handler.removeCallbacks(this);
                handler.postDelayed(this, 60000);
            }

        }
    }
    
    // Sensor managers
    public void checkDeviceSensors(boolean workerNeedsSensors) {

        // Check if device sensors are required, and turn them on if necessary
        if (workerNeedsSensors == true && usingDeviceSensors == false) {

            // Register sensors
            sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
            if (sensorManager != null) {

                if (sensorManager.registerListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                        SensorManager.SENSOR_DELAY_NORMAL))
                    dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_ACCELEROMETER);
                else
                    dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_USE_ACCELEROMETER);

                if (sensorManager.registerListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
                        SensorManager.SENSOR_DELAY_NORMAL))
                    dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_MAGNETIC_FIELD);
                else
                    dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_USE_MAGNETIC_FIELD);

                if (sensorManager.registerListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT), SensorManager.SENSOR_DELAY_NORMAL))
                    dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_LIGHT);
                else
                    dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_USE_LIGHT);

                usingDeviceSensors = true;
                dInfo.setFlag(DiagnosticInfo.DIAG_USING_SENSORS);

            } else {

                dInfo.clearFlag(DiagnosticInfo.DIAG_USING_SENSORS);

            }
        } else if (workerNeedsSensors == false && usingDeviceSensors == true) {

            // Unregister sensors
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER));
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD));
            sensorManager.unregisterListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT));

            usingDeviceSensors = false;
            dInfo.clearFlag(DiagnosticInfo.DIAG_USING_SENSORS);

        }
    }

    public void checkGPS(boolean workerNeedsGPS) {

        // Check if GPS is required, and turn it on if necessary
        if (workerNeedsGPS == true && usingGPSSensors == false) {

            // Register GPS
            if (locManager != null) {
                locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0.0f, locListener);
                usingGPSSensors = true;
                dInfo.setFlag(DiagnosticInfo.DIAG_USING_GPS);

            } else {

                dInfo.clearFlag(DiagnosticInfo.DIAG_USING_GPS);

            }
        } else if (workerNeedsGPS == false && usingGPSSensors == true) {

            // Unregister sensors
            if (locManager != null) {
                locManager.removeUpdates(locListener);
            }

            usingGPSSensors = false;
            dInfo.clearFlag(DiagnosticInfo.DIAG_USING_GPS);

        }
    }

    BroadcastReceiver onReceiveBroadcast = new BroadcastReceiver() {

        // Deal with received broadcasts
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (action.equals(Intent.ACTION_SCREEN_ON)) {

                set(s_cpu_usage, 0.0f); // For aesthetics

                // Refresh all worker runners
                WorkerRunnable runnerWorker;
                for (Enumeration<WorkerRunnable> e = runnerTable.elements(); e.hasMoreElements();) {
                    runnerWorker = e.nextElement();
                    handler.removeCallbacks(runnerWorker);
                    runnerWorker.refresh();
                    handler.post(runnerWorker);
                }

            } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {

                set(s_cpu_usage, 0.0f); // For aesthetics

            } else if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {

                // Set battery level
                int level = intent.getIntExtra("level", 1);
                int scale = intent.getIntExtra("scale", 1);
                set(s_battery_level, (float) level * 100.0f / (float) scale);

                // Set battery temperature
                int temperature = intent.getIntExtra("temperature", 1);
                set(s_temperature, (float) temperature / 10.0F);

            } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {

                int sWiFi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, (int) signalMin); // In dBm
                set(s_wifi_signal, (float) sWiFi);
                // set(s_wifi_signal, (float)(1.0E9*Math.pow(10.0,(float)sWiFi/10.0))); // Converted to picoW

            } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
                if (intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) != WifiManager.WIFI_STATE_ENABLED) {

                    // WiFi disabled, set value to zero
                    set(s_wifi_signal, signalMin);
                }
            } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                NetworkInfo networkInfo = (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                if (networkInfo == null) {

                    // No network, set WiFi value to zero
                    set(s_wifi_signal, signalMin);

                } else if (networkInfo.isConnected() == false) {

                    // WiFi disconnected, set value to zero
                    set(s_wifi_signal, signalMin);
                }
            }
        }
    };
    
    private SensorEventListener sensorEventListener = new SensorEventListener() {

        // Asynchronous device sensors
        public void onSensorChanged(SensorEvent se) {

            if (se.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                setAverage(s_gravity, se.values[1]);

            } else if (se.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
                setAverage(s_magnetic_field, se.values[0] * se.values[0] + se.values[1] * se.values[1] + se.values[2] * se.values[2]);

            } else if (se.sensor.getType() == Sensor.TYPE_LIGHT) {
                setAverage(s_light, se.values[0]);
            }

        }

        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }
    };

    PhoneStateListener phoneStateListener = new PhoneStateListener() {

        @Override
        // Asynchronous telephony signal strength sensor
        public void onSignalStrengthChanged(int asu) {
            super.onSignalStrengthChanged(asu);

            float sg_Val_dBm = signalMin;
            if (asu != -1)
                sg_Val_dBm = -113.0f + 2.0f * asu;
            set(s_cell_signal, (float) sg_Val_dBm);
        }
    };

    LocationListener locListener = new LocationListener() {

        // Asynchronous GPS speed and altitude sensor
        public void onLocationChanged(Location location) {

            // Set speed
            if (location.hasSpeed()) {
                set(s_gps_speed, (float) location.getSpeed());
                dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_GPS_SPEED);

            } else {
                dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_USE_GPS_SPEED);
            }

            // Set altitude
            if (location.hasAltitude()) {
                set(s_gps_altitude, (float) location.getAltitude());
                dInfo.setFlag(DiagnosticInfo.DIAG_CAN_USE_GPS_ALT);

            } else {
                dInfo.clearFlag(DiagnosticInfo.DIAG_CAN_USE_GPS_ALT);
            }

        }

        public void onProviderDisabled(String provider) {
        }

        public void onProviderEnabled(String provider) {
        }

        public void onStatusChanged(String provider, int status, Bundle extras) {
        }
    };

}
